ref 这一种访问 DOM 的主要方式。然而,useRef() 比 ref 属性更有用。它可以很方便地保存任何可变值,其类似于在 class 中使用实例字段的方式.
当 ref 对象内容发生变化时,useRef 并不会通知你。变更 .current 属性不会引发组件重新渲染。如果想要在 React 绑定或解绑 DOM 节点的 ref 时运行某些代码,则需要使用回调 ref 来实现。
场景 1:只在更新时运行 useEffect
使用一个可变的 ref 手动存储一个布尔值来表示是首次渲染还是后续渲染.
jsx
function Example() {
const [count, setCount] = useState(0);
const prevCountRef = useRef(false);
useEffect(() => {
if (prevCountRef.current) {
console.log("只在更新时候执行");
} else {
console.log("首次渲染执行");
prevCountRef.current = true;
}
});
return (
<div>
<div>{count}</div>
<button
onClick={() => {
setCount(count + 1);
}}
>
+
</button>
</div>
);
}抽成自定义 hook:
jsx
function Example() {
const [count, setCount] = useState(0);
const update = useUpdate();
console.log(update, "是否更新");
return (
<div>
<div>{count}</div>
<button
onClick={() => {
setCount(count + 1);
}}
>
+
</button>
</div>
);
}
function useUpdate() {
const ref = useRef(false);
useEffect(() => {
ref.current = true;
});
return ref.current;
}场景 2:获取上一轮的 props 或 state
为什么 ref.current 拿到是上次的值?原因:
- useEffect 很重要的一点是:它是在每次渲染之后才会触发的,是延迟执行的。
- return 语句是同步的,所以 return 的时候,ref.current 还是旧值。
- 以下代码的执行顺序是 1 3 2
jsx
function App() {
const [count, setCount] = useState(0);
const prevCountRef = useRef();
console.log("1", count);
useEffect(() => {
console.log("2.", count);
prevCountRef.current = count;
});
const prevCount = prevCountRef.current;
console.log(`3.之前的状态: ${prevCount};现在状态: ${count}`);
return (
<div>
<div>{count}</div>
<button
onClick={() => {
setCount(count + 1);
}}
>
+
</button>
</div>
);
}抽取成自定义 Hook:
jsx
export default function usePrevious(value) {
const ref = useRef();
useEffect(() => {
ref.current = value;
}, [value]);
return ref.current;
}场景 3:解决 hooks 时,由于异步闭包无法获取最新 state 的问题
jsx
import React, { useState, useEffect, useRef } from "react";
const RefComponent = () => {
// 使用 useState 存放和改变展示的 number
const [number, setNumber] = useState(0);
// 使用 useRef 生成一个独立的 ref 对象
// 在它的 current 属性单独存放一个展示的 number, 初始值为 0
const numRef = useRef(0);
function incrementAndDelayLogging() {
// 点击按钮 number + 1
setNumber(number + 1);
// 同时 ref 对象的 current 属性值也 + 1
numRef.current++;
// 定时器函数中产生了闭包, 这里 number 的是组件更新前的 number 对象, 所以值一直会滞后 1
setTimeout(() => alert(`state: ${number} | ref: ${numRef.current}`), 1000);
}
// 直接渲染的组件是正常情况, 可以获取到最新的 state,
// 所以 ref.current 和 state 存储的值显示一致
return (
<div>
<h1>solving closure by useRef</h1>
<button onClick={incrementAndDelayLogging}>alert in setTimeout</button>
<h4>state: {number}</h4>
<h4>ref: {numRef.current}</h4>
</div>
);
};场景 4:记录 DOM 尺寸并避免重复测量
jsx
function useDomRect<T extends HTMLElement>() {
const ref = useRef<T | null>(null);
const rectRef = useRef<DOMRect>();
useLayoutEffect(() => {
if (ref.current) {
rectRef.current = ref.current.getBoundingClientRect();
}
});
return { ref, rect: rectRef.current };
}useLayoutEffect 保证在浏览器绘制前拿到尺寸,而 rectRef 缓存住上一次的 DOMRect,方便 diff。
场景 5:暴露命令式句柄
ts
export interface PhotoHandle {
focus: () => void;
}
const Photo = forwardRef<PhotoHandle>((props, ref) => {
const inputRef = useRef<HTMLInputElement>(null);
useImperativeHandle(ref, () => ({
focus() {
inputRef.current?.focus();
},
}));
return <input ref={inputRef} />;
});Ref 不只有 DOM,它还可以承载自定义的命令式 API,通过 useImperativeHandle 暴露给父组件。
纠错与补充
useRef持有的对象在整个生命周期内都不会变化,因此非常适合保存“不会触发渲染的状态”;如果你发现某段逻辑需要既读又更新 UI,就应该改回useState。- 在严格模式下 React 会执行组件初始化两次,但
useRef始终返回同一个引用,因此不会重复注册事件;但你仍然需要在副作用里正确清理监听器,防止ref.current指向已经卸载的节点。 useRef(null)并不会立刻赋值,需要到render之后 React 才会把 DOM 节点挂到current上,所以在事件回调里使用前务必判空。